/**
 * Software License, Version 1.0
 * 
 * Copyright 2008 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgment:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgment may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or Granules, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called Granules, 
 * nor may Indiana University or Community Grids Lab or Granules appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 * LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 * WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 * NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 * GENERATED USING SOFTWARE.
 */

package cgl.granules.dataset;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Enumeration;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import cgl.granules.util.MarshallingUtil;

/**
 * An implementation of the <code>FileAccess</code> dataset interface.
 * 
 * @author Shrideep Pallickara
 * 
 */
public class FileDataset implements FileAccess {

    private int datasetType = FILES;

    private String datasetIdentifier;
    private String datasetDescription;
    private Properties filesAndPermissions;

    private boolean isInitialized = true;
    private boolean needsInitialization = true;
    private boolean isAvailable = false;

    private AtomicLong lastModificationTime;
    private ConcurrentHashMap<String, FileChannel> fileChannels;
    private String temporaryDirectoryString = "C:/Temp/";
    private File temporaryDirectory = null;


    public FileDataset(String datasetIdentifier, String datasetDescription) {
        this.datasetIdentifier = datasetIdentifier;
        this.datasetDescription = datasetDescription;
        filesAndPermissions = new Properties();
    }


    public FileDataset(byte[] marshalledBytes) throws IOException,
        DatasetException {
        ByteArrayInputStream baInputStream =
            new ByteArrayInputStream(marshalledBytes);
        DataInputStream din = new DataInputStream(baInputStream);
        MarshallingUtil marshaller = MarshallingUtil.getInstance();

        datasetType = din.readInt();
        datasetIdentifier = marshaller.readString(din);
        datasetDescription = marshaller.readString(din);

        needsInitialization = din.readBoolean();

        filesAndPermissions = marshaller.readProperties(din);
        initializeDataset();
    }


    @Override
    public byte[] getBytes() throws IOException {
        MarshallingUtil marshaller = MarshallingUtil.getInstance();
        byte[] marshalledBytes = null;
        ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(baOutputStream);

        dout.writeInt(datasetType);
        marshaller.writeString(dout, datasetIdentifier);
        marshaller.writeString(dout, datasetDescription);
        dout.writeBoolean(needsInitialization);

        marshaller.writeProperties(dout, filesAndPermissions);

        dout.flush();
        marshalledBytes = baOutputStream.toByteArray();
        return marshalledBytes;
    }

    
    @Override
    public Enumeration getFilenames() {
        return filesAndPermissions.keys();
    }
    

    @Override
    public boolean close() throws DatasetException {
        Enumeration<FileChannel> e = fileChannels.elements();
        String failedOnes = "";
        boolean problems = false;
        while (e.hasMoreElements()) {
            FileChannel fileChannel = e.nextElement();
            try {
                fileChannel.close();
            } catch (IOException ioe) {
                problems = true;
                failedOnes +=
                    (fileChannel.toString() + " because " + ioe.toString() + "\n");
            }
        }
        if (problems) {
            String errorReport =
                "Problems closing the following files :\n" + failedOnes;
            throwDatasetException(errorReport);
        }

        return true;
    }


    @Override
    public void addFile(String filename, String permissions)
        throws DatasetException {
        if (filename == null || permissions == null) {
            throw new NullPointerException("Either the filename (" + filename
                + "OR permissions (" + permissions + " is null.");
        }
        if (filesAndPermissions.containsKey(filename)) {
            String errorReport =
                "The file (" + filename + ") already exists in this dataset";
            throwDatasetException(errorReport);
        }

        if (permissions.equals("r") || permissions.equals("w")
            || permissions.equals("rw")) {
        } else {
            permissions = "rw";
        }

        filesAndPermissions.put(filename, permissions);
    }


    @Override
    public void removeFile(String filename) throws DatasetException {
        if (filename == null) {
            throw new NullPointerException("File submitted for removal is null");
        }

        if (!filesAndPermissions.containsKey(filename)) {
            String errorReport =
                "The file (" + filename + ") is not part of this dataset";
            throwDatasetException(errorReport);
        }

        filesAndPermissions.remove(filename);
    }


    @Override
    public DataAvailabilityNotifier getDataAvailabilityNotifier() {
        return null;
    }


    @Override
    public int getDatasetType() {
        return datasetType;
    }


    @Override
    public String getDescription() {
        return datasetDescription;
    }


    @Override
    public String getIdentifier() {
        return datasetIdentifier;
    }


    @Override
    public long getLastModificationTime() {
        return lastModificationTime.get();
    }


    public Properties getInitializationDirectives() {
        return filesAndPermissions;
    }


    @Override
    public boolean initializeDataset() throws DatasetException {
        if (isInitialized) {
            return true;
        }
        /*
         * temporaryDirectory = new File(temporaryDirectoryString); if
         * (!temporaryDirectory.exists()) { temporaryDirectory.mkdirs(); }
         */
        initializeFiles(filesAndPermissions);
        isInitialized = true;
        isAvailable = true;

        return isInitialized;
    }


    @Override
    public boolean isAvailable() {
        return isAvailable;
    }


    @Override
    public boolean isDataAvailable() {
        // TODO Auto-generated method stub
        return false;
    }


    @Override
    public boolean needsInitialization() {
        return needsInitialization;
    }


    @Override
    public boolean isInitialized() {
        return isInitialized;
    }


    @Override
    public boolean supportsDataAvailabilityNotifications() {
        return false;
    }


    private void initializeFiles(Properties props) throws DatasetException {
        fileChannels = new ConcurrentHashMap<String, FileChannel>();
        if (props == null) {
            return;
        }

        Enumeration<?> e = props.propertyNames();
        while (e.hasMoreElements()) {
            String fileName = (String) e.nextElement();
            String permissions = props.getProperty(fileName);
            initializeFile(fileName, permissions);
        }
    }


    private void initializeFile(String filename, String permissions)
        throws DatasetException {
        File file = new File(filename);
        String operation = null;
        if (permissions.equals("r") || permissions.equals("w")
            || permissions.equals("rw")) {
        } else {
            permissions = "rw";
        }

        

        try {
            // String filenameInTempDirectory =
            // getFilenameInTemporaryDirectory(filename);
            FileChannel fileChannel = null;

            if (permissions.equals("r")) {
                operation = "Reading";
                FileInputStream fis = new FileInputStream(file);
                fileChannel = fis.getChannel();
            }

            if (permissions.equals("w")) {
                operation = "Writing";

                createDirectoryIfNeeded(filename);
                
                file = new File(filename);
                if (!file.exists()) {
                    file.createNewFile();
                }
                FileOutputStream fos = new FileOutputStream(file);
                fileChannel = fos.getChannel();
            }

            if (permissions.equals("rw")) {
                operation = "Reading + Writing";
                createDirectoryIfNeeded(filename);
                RandomAccessFile raf = new RandomAccessFile(filename, "rw");
                fileChannel = raf.getChannel();
            }

            fileChannels.put(filename, fileChannel);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            String errorReport =
                "Unable to locate file (" + filename + ") for " + operation
                    + ":  " + e.getMessage();
            throwDatasetException(errorReport);
        } catch (IOException e) {
            String errorReport =
                "Problems creating file (" + filename + ") for " + operation
                + e.getMessage() + e.fillInStackTrace();
            throwDatasetException(errorReport);
        }

    }

    
    private void createDirectoryIfNeeded(String filename) {
        String directory = retrieveDirectoryInfo(filename);
        File directoryFile = new File(directory);
        if (!directoryFile.exists()) {
            directoryFile.mkdirs();
            System.out.println("Created intermediate directories "
                + directory);
        }
    }

    /**
     * This method needs to be used to ensure that files are written only in the
     * temporary directory. This featuyre will be activated in a later version
     * of this dataset.
     * 
     * @param fileName
     * @return
     * @throws IOException
     */
    private String getFilenameInTemporaryDirectory(String fileName)
        throws IOException {
        String directory = retrieveDirectoryInfo(fileName);

        String slashIndicator = "/";
        temporaryDirectoryString =
            pruneTrailingSlash(temporaryDirectoryString, slashIndicator);
        directory = pruneLeadingSlash(directory, slashIndicator);

        String baseDirectory = temporaryDirectoryString + "/" + directory;

        File directoryFile = new File(baseDirectory);
        if (!directoryFile.exists()) {
            directoryFile.mkdirs();
            System.out.println("Created intermediate directories "
                + baseDirectory);
        }

        String fileNameInTempDirectory = "";
        int index = fileName.lastIndexOf("/");
        if (index != 0) {
            String standAloneFileName =
                fileName.substring(index, fileName.length());
            fileNameInTempDirectory = baseDirectory + standAloneFileName;

        } else {
            fileNameInTempDirectory = baseDirectory + fileName;
        }

        return fileNameInTempDirectory;
    }


    private String retrieveDirectoryInfo(String fileName) {

        int index = fileName.lastIndexOf("/");
        if (index == 0) {
            return null;
        }
        String directory = fileName.substring(0, index);
        return directory;

    }


    /**
     * Prunes the leading slash within a String, based on the specified
     * slashIndicator
     * 
     * @param element
     *            The element that might be pruned
     * @param slashIndicator
     *            This could be either "/" or "\"
     * @return The pruned element
     */
    private String pruneLeadingSlash(String element, String slashIndicator) {
        if (element.startsWith(slashIndicator)) {
            System.out.println("Received = " + element);
            element = element.substring(1, element.length());
            System.out.println("Pruned leading slash = " + element);
        }
        return element;
    }


    /**
     * Prunes the trailing slash within a String, based on the specified
     * slashIndicator
     * 
     * @param element
     *            The element that might be pruned
     * @param slashIndicator
     *            This could be either "/" or "\"
     * @return The pruned element
     */
    private String pruneTrailingSlash(String element, String slashIndicator) {
        if (element.endsWith(slashIndicator)) {
            System.out.println("Received = " + element);
            element = element.substring(0, element.length() - 1);
            System.out.println("Pruned trailing slash = " + element);
        }
        return element;
    }


    @Override
    public int read(String filename, byte[] bytesToRead)
        throws DatasetException, IOException {
        ByteBuffer wrapBuffer = ByteBuffer.wrap(bytesToRead);
        return read(filename, wrapBuffer);

    }


    @Override
    public int read(String fileName, ByteBuffer readBuffer)
        throws DatasetException, IOException {
        String errorReport = null;

        if (!fileChannels.containsKey(fileName)) {
            errorReport =
                "Trying to read a file (" + fileName
                    + ") that was not specified in the original dataset";
            throwDatasetException(errorReport);
        }

        readBuffer.clear();
        FileChannel fileChannel = fileChannels.get(fileName);
        int bytesRead = fileChannel.read(readBuffer);
        return bytesRead;
    }


    @Override
    public int write(String fileName, byte[] bytesToWrite)
        throws DatasetException, IOException {
        ByteBuffer wrapBuffer = ByteBuffer.wrap(bytesToWrite);

        System.out.println("Writing byte[] of length = " + bytesToWrite.length);
        wrapBuffer.position(wrapBuffer.limit());
        return write(fileName, wrapBuffer);

    }


    @Override
    public int write(String fileName, ByteBuffer writeBuffer)
        throws DatasetException, IOException {

        if (!fileChannels.containsKey(fileName)) {
            initializeFile(fileName, "rw");
        }

        FileChannel fileChannel = fileChannels.get(fileName);

        writeBuffer.flip();
        int numOfBytesWritten = fileChannel.write(writeBuffer);
        fileChannel.force(true);

        return numOfBytesWritten;
    }


    @Override
    public long getPosition(String fileName) throws DatasetException, IOException {
        String errorReport = null;
        if (!fileChannels.containsKey(fileName)) {
            errorReport =
                "Trying to get position for a file (" + fileName
                    + ") that is not part of this dataset";
            throwDatasetException(errorReport);
        }
        FileChannel fileChannel = fileChannels.get(fileName);

        return fileChannel.position();
    }


    @Override
    public void setPosition(String fileName, long newPosition)
        throws DatasetException, IOException {
        String errorReport = null;
        if (!fileChannels.containsKey(fileName)) {
            errorReport =
                "Trying to set position for a file (" + fileName
                    + ") that is not part of this dataset";
            throwDatasetException(errorReport);
        }
        FileChannel fileChannel = fileChannels.get(fileName);

        fileChannel.position(newPosition);
    }


    @Override
    public long size(String fileName) throws DatasetException, IOException {
        String errorReport = null;
        if (fileChannels == null) {
            System.out.println("FileChannels == null");
        }
        if (!fileChannels.containsKey(fileName)) {
            errorReport =
                "Trying to get the size of a file (" + fileName
                    + ") that is not part of this dataset";
            throwDatasetException(errorReport);
        }
        FileChannel fileChannel = fileChannels.get(fileName);

        return fileChannel.size();
    }


    @Override
    public boolean isPartOfDataset(String fileName) {
        return fileChannels.containsKey(fileName);
    }


    /**
     * Throws a data set exception based on the specified error report
     * 
     * @param errorReport
     * @throws DatasetException
     */
    private void throwDatasetException(String errorReport)
        throws DatasetException {
        DatasetException dse = new DatasetException(errorReport);
        throw dse;
    }


    /**
     * @param args
     */
    public static void main(String[] args) {
        try {
            Properties props = new Properties();
            String file1 = "Datasets/Files/file1.txt";
            String file2 = "Datasets/Files/file2.txt";
            props.put(file1, "w");
            props.put(file2, "rw");
            FileDataset fileDataset =
                new FileDataset("DatasetID:123", "Descr: Datsset");

            fileDataset.addFile(file1, "w");
            fileDataset.addFile(file2, "rw");
            fileDataset.initializeDataset();
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            buffer.putInt(Integer.MAX_VALUE);
            buffer.put("This is a test of the writing capabilities".getBytes());
            buffer.putLong(Long.MAX_VALUE);

            int written = 0;
            // if (buffer.hasArray()) {
            // byte[] toWrite = buffer.array();
            // written = fileDataset.write(file2, toWrite);
            // System.out.println("Wrote " + written + " byte[] bytes");
            // } else {
            written = fileDataset.write(file2, buffer);
            System.out.println("Wrote " + written + " Buffer bytes");
            // }

            fileDataset.setPosition(file2, 0);
            int read = fileDataset.read(file2, buffer);

            System.out.println("Number of bytes read = " + read);
            buffer.rewind();
            System.out.println(buffer.getInt());
            System.out.println(Integer.MAX_VALUE);

        } catch (DatasetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}
